package dungeonPackage;

import java.util.HashSet;
import java.util.Set;

import squareBorderPackage.Direction;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class of orientations involving an x-coordinate, an y-coordinate and a
 * z-coordinate.
 * 
 * @invar 	The x-coordinate of each orientation must be a valid coordinate for an orientation. 
 *        	| isValidCoordinate(getX)
 * @invar 	The y-coordinate of each orientation must be a valid coordinate for an orientation. 
 * 			| isValidCoordinate(getY)
 * @invar 	The z-coordinate of each orientation must be a valid coordinate for an orientation. 
 * 			| isValidCoordinate(getZ)
 * 
 * @version 1.0
 * @author Jessa Bekker & Esther Alberts
 */
@Value
public class Orientation {

	/**
	 * Initialize this Orientation with the given coordinates.
	 * 
	 * @param 	x
	 *          The x-coordinate.
	 * @param 	y
	 *          The y-coordinate.
	 * @param 	z
	 *          The z-coordinate.
	 * @post 	The x-coordinate of the orientation is equal to the given x.
	 *       	| new.getX() == x
	 * @post 	The y-coordinate of the orientation is equal to the given y.
	 *       	| new.getY() == y
	 * @post 	The z-coordinate of the orientation is equal to the given z.
	 *       	| new.getZ() == z
	 * @throws 	IllegalArgumentException
	 *          One or more of the given coordinates are not effective. 
	 *          | isValidCoordinate(x) || isValidCoordinate(y) || isValidCoordinate(z)
	 */
	public Orientation(long x, long y, long z) throws IllegalArgumentException {
		setX(x);
		setY(y);
		setZ(z);
	}

	/**
	 * Check whether the given coordinate is a valid coordinate.
	 * 
	 * @param 	coordinate
	 *          | The coordinate to check.
	 * @return 	False if the given coordinate is smaller than zero. 
	 * 			| if (coordinate < 0) 
	 * 			| then( result == false)
	 */
	public static boolean isValidCoordinate(long coordinate) {
		if (coordinate < 0)
			return false;
		if (coordinate > Long.MAX_VALUE)
			return false;
		return true;
	}

	/**
	 * Return the x-coordinate.
	 */
	@Basic
	@Immutable
	public long getX() {
		return x;
	}

	/**
	 * Set the x-coordinate to the given coordinate.
	 * 
	 * @param 	x
	 *          The coordinate to be registered as x-coordinate.
	 * @post 	the new x-coordinate is equal to the given coordinate. 
	 * 			| new.getX() == x
	 * @throws 	IllegalArgumentException
	 *			The given coordinate can't be used as coordinate. 
	 *			| ! isValidCoordinate(x)
	 */
	private void setX(long x) throws IllegalArgumentException {
		if (!isValidCoordinate(x))
			throw new IllegalArgumentException("Non effective x-coordinate");
		this.x = x;
	}

	/**
	 * Variable registering the y-coordinate.
	 */
	private long x;

	/**
	 * Return the y-coordinate.
	 */
	@Basic
	@Immutable
	public long getY() {
		return y;
	}

	/**
	 * Set the y-coordinate to the given coordinate.
	 * 
	 * @param 	y
	 *          The coordinate to be registered as y-coordinate.
	 * @post 	the new y-coordinate is equal to the given coordinate. 
	 * 			| new.getY() == y
	 * @throws 	IllegalArgumentException
	 *          The given coordinate can't be used as coordinate. 
	 *          | !isValidCoordinate(y)
	 */
	private void setY(long y) throws IllegalArgumentException {
		if (!isValidCoordinate(y))
			throw new IllegalArgumentException("Non effective y-coordinate");
		this.y = y;
	}

	/**
	 * Variable registering the z-coordinate.
	 */
	private long y;

	/**
	 * Return the z-coordinate.
	 */
	@Basic
	@Immutable
	public long getZ() {
		return z;
	}

	/**
	 * Set the z-coordinate to the given coordinate.
	 * 
	 * @param 	z
	 *			The coordinate to be registered as z-coordinate.
	 * @post 	the new z-coordinate is equal to the given coordinate.
	 * 			| new.getZ() == z
	 * @throws 	IllegalArgumentException
	 *          The given coordinate can't be used as coordinate. 
	 *          | !isValidCoordinate(z)
	 */
	private void setZ(long z) throws IllegalArgumentException {
		if (!isValidCoordinate(z))
			throw new IllegalArgumentException("Non effective z-coordinate");
		this.z = z;
	}

	/**
	 * Variable registering the z-coordinate.
	 */
	private long z;
	
	/**
	 * Return an array with the directions in which the this orientation and the
	 * given orientation have borders.
	 * 
	 * @param 	other
	 *          The orientation to check in which direction it borders this.
	 * @return
	 * @throws 	IllegalArgumentException
	 *          The given orientation is not effective or this orientation
	 *          and the given orientation are no neighbors. 
	 *          | other == null || !this.isTangentTo(other)
	 */
	public Direction[] getSharedBorderDirections(Orientation other)
			throws IllegalArgumentException {
		Direction[] directions = new Direction[2];
		if (other == null)
			throw new IllegalArgumentException("non effective orientation");
		if (!this.isTangentTo(other))
			throw new IllegalArgumentException(
					"the given no neigbor of this orientation");
	
		if (Orientation.isValidCoordinate(other.getX()-1)
			&& this.equals(new Orientation(other.getX() - 1, other.getY(), other
				.getZ()))) {
			directions[0] = Direction.EAST;
			directions[1] = Direction.WEST;
		} else if (Orientation.isValidCoordinate(other.getX()+1)
				&& this.equals(new Orientation(other.getX() + 1, other.getY(),
				other.getZ()))) {
			directions[0] = Direction.WEST;
			directions[1] = Direction.EAST;
		} else if (Orientation.isValidCoordinate(other.getY()-1)
				&& this.equals(new Orientation(other.getX(), other.getY() - 1,
				other.getZ()))) {
			directions[0] = Direction.NORTH;
			directions[1] = Direction.SOUTH;
		} else if (Orientation.isValidCoordinate(other.getY()+1)
				&& this.equals(new Orientation(other.getX(), other.getY() + 1,
				other.getZ()))) {
			directions[0] = Direction.SOUTH;
			directions[1] = Direction.NORTH;
		} else if (Orientation.isValidCoordinate(other.getZ()-1)
				&& this.equals(new Orientation(other.getX(), other.getY(),
				other.getZ() - 1))) {
			directions[0] = Direction.CEILING;
			directions[1] = Direction.FLOOR;
		} else {
			directions[0] = Direction.FLOOR;
			directions[1] = Direction.CEILING;
		}
		return directions;
	}

	/**
	 * Return the orientations tangentTo this orientation but outside the box with given minCo en maxCo
	 * 
	 * @param 	minCo
	 * 			The minimum orientation of the box
	 * @param 	maxCo
	 * 			The maximum orientation of the box
	 * @pre		This orientation must be an edgeOrientation of the box defined by the given min and maxCoordinates.
	 * @return	
	 */
	public HashSet<Orientation> getOutsideEdges(Orientation minCo, Orientation maxCo){
		HashSet<Orientation> orientations = new HashSet<Orientation>();
		
		for(Orientation orientation : this.getTangentOrientations()){
			if(orientation.isOutside(maxCo) || !orientation.isLargerThanOrEqualTo(minCo))
				orientations.add(orientation);
		}
		
		return orientations;
	}

	/**
	 * Return set with the orientations that border this orientation.
	 * 
	 * @return 	A set with the orientations that border this orientation.
	 */
	public Set<Orientation> getTangentOrientations() {
		Set<Orientation> neighbors = new HashSet<Orientation>();
		if(isValidCoordinate(getX() + 1))
			neighbors.add(new Orientation(getX() + 1, getY(), getZ()));
		if(isValidCoordinate(getX() -1))
			neighbors.add(new Orientation(getX() - 1, getY(), getZ()));
		if(isValidCoordinate(getY() + 1))
			neighbors.add(new Orientation(getX(), getY() + 1, getZ()));
		if(isValidCoordinate(getY() - 1))
			neighbors.add(new Orientation(getX(), getY() - 1, getZ()));
		if(isValidCoordinate(getZ() + 1))
			neighbors.add(new Orientation(getX(), getY(), getZ() + 1));
		if(isValidCoordinate(getZ() - 1))
			neighbors.add(new Orientation(getX(), getY(), getZ() - 1));

		return neighbors;
	}

	/**
	 * Check whether the given orientation is a neighbor of this orientation
	 * 
	 * @param 	orientation
	 *          The orientation to check for being a neighbor of this neighbor.
	 * @return 	True if and only if the other orientation is a neighbor of this orientation. 
	 * 			| result == getNeighbors().contains(other)
	 * @throws 	IllegalArgumentException
	 *			The given orientation is not effective. 
	 *			| (other == null)
	 */
	public boolean isTangentTo(Orientation other)
			throws IllegalArgumentException {
		if (other == null)
			throw new IllegalArgumentException("non valid orientation");
		return getTangentOrientations().contains(other);
	}

	/**
	 * Check whether this orientation is outside the cuboid defined by the other
	 * orientation.
	 * 
	 * @param 	other
	 *          The orientation to compare this orientation with.
	 * @return 	True if and only if this orientation is outside the cuboid
	 *         	defined by the other orientation. 
	 *         	| result == this.getX() > other.getX() || this.getY() > other.getY() || this.getZ() > other.getZ() )
	 * @throws 	IllegalArgumentException
	 *          The given orientation is not effective. 
	 *          | (other == null)
	 */
	public boolean isOutside(Orientation other) throws IllegalArgumentException {
		if (other == null)
			throw new IllegalArgumentException("non valid orientation");
		return (this.getX() > other.getX() || this.getY() > other.getY())
				|| this.getZ() > other.getZ();
	}

	/**
	 * Check whether this orientation is larger the other orientation.
	 * 		Check whether each coordinate of this orientation is larger than
	 * 		the corresponding coordinate of the other orientation. 
	 * 
	 * @param 	other
	 *          The orientation to compare this orientation with.
	 * @return 	True if and only if this orientation is larger than the other orientation. 
	 *         	| result == this.getX() > other.getX() && this.getY() > other.getY() && this.getZ() > other.getZ() )
	 * @throws 	IllegalArgumentException
	 *          The given orientation is not effective. 
	 *          | (other == null)
	 */
	public boolean isLargerThan(Orientation other) throws IllegalArgumentException {
		if (other == null)
			throw new IllegalArgumentException("non valid orientation");
		return (this.getX() > other.getX() && this.getY() > other.getY())
				&& this.getZ() > other.getZ();
	}
	
	/**
	 * Check whether this orientation is larger than or equal to the other orientation.
	 * 		Check whether each coordinate of this orientation is larger than or equal to
	 * 		the corresponding coordinate of the other orientation. 
	 * 
	 * @param 	other
	 *          The orientation to compare this orientation with.
	 * @return 	True if and only if this orientation is larger than or equal to the other orientation. 
	 *         	| result == this.getX() >= other.getX() && this.getY() >= other.getY() && this.getZ() >= other.getZ() )
	 * @throws 	IllegalArgumentException
	 *          The given orientation is not effective. 
	 *          | (other == null)
	 */
	public boolean isLargerThanOrEqualTo(Orientation other) throws IllegalArgumentException {
		if (other == null)
			throw new IllegalArgumentException("non valid orientation");
		return (this.getX() >= other.getX() && this.getY() >= other.getY())
				&& this.getZ() >= other.getZ();
	}
	
	/**
	 * Add the given orientation to this orientation.
	 * 
	 * @param 	other
	 * 			The orientation to add to this orientation.
	 * @return	A new orientation with the coordinates of the given orientation
	 * 			added to the coordinates of this orientation.
	 * 			| result.getX() == this.getX() + other.getX()
	 * 			|	&& result.getY() == this.getY() + other.getY()
	 * 			|	&& result.getZ() == this.getZ() + other.getZ();
	 * @throws 	IllegalArgumentException
	 * 			The given orientation is not effective
	 * 			| other == null 
	 */
	public Orientation add(Orientation other) throws IllegalArgumentException {
		if (other == null)
			throw new IllegalArgumentException("non valid orientation");
		return new Orientation(this.getX()+other.getX(), this.getY()+other.getY(), this.getZ()+other.getZ());
	}

	/**
	 * Subtract the given orientation to this orientation.
	 * 
	 * @param 	other
	 * 			The orientation to subtract from this orientation.
	 * @return	A new orientation with the coordinates of the given orientation
	 * 			subtracted from the coordinates of this orientation.
	 * 			| result.getX() == this.getX() - other.getX()
	 * 			|	&& result.getY() == this.getY() - other.getY()
	 * 			|	&& result.getZ() == this.getZ() - other.getZ();
	 * @throws 	IllegalArgumentException
	 * 			The given orientation is not effective
	 * 			| other == null 
	 */
	public Orientation subtract(Orientation other) throws IllegalArgumentException {
		if (other == null)
			throw new IllegalArgumentException("non valid orientation");
		return new Orientation(this.getX()-other.getX(), this.getY()-other.getY(), this.getZ()-other.getZ());
	}

	/**
	 * Check whether this orientation is equal to the given object.
	 * 
	 * @return True if and only if the given object is effective, and if this
	 *         orientation and the given object belong to the same class, and if
	 *         this orientation and the other object longerpreted as an
	 *         orientation have equal x-coordinates, y-coordinates and
	 *         z-coordinates. 
	 *         | result == 
	 *         | ( (other != null) 
	 *         | && (this.getClass() == other.getClass()) 
	 *         | && (this.getX().equals( 
	 *         | ((Orientation other).getX()))
	 *         | && (this.getY().equals( 
	 *         | ((Orientation other).getY())) 
	 *         | && (this.getZ().equals( 
	 *         | ((Orientation other).getZ()))
	 */
	@Override
	public boolean equals(Object other) {
		if (other == null)
			return false;
		if (this.getClass() != other.getClass())
			return false;
		Orientation otherOrientation = (Orientation) other;
		return ((this.getX() == otherOrientation.getX())
				&& (this.getY() == otherOrientation.getY()) && (this.getZ() == otherOrientation
				.getZ()));
	}

	/**
	 * Return the hash code for this orientation.
	 */
	@Override
	public int hashCode() {
		return (int) getX() + (int) getY() + (int) getZ();
	}

	/**
	 * Return a textual representation of this orientation.
	 * 
	 * @return A string consisting of the textual representation of the
	 *         x-coordinate, the y-coordinate and the z-coordinate, separated by
	 *         comma's and enclosed with parentheses. 
	 *         | result.equals( 
	 *         | "("+getX() +"," + getY() + "," + getZ() + ")"
	 */
	@Override
	public String toString() {
		return "(" + getX() + "," + getY() + "," + getZ() + ")";
	}

}
